﻿/**
 * \brief Class for generic players in KaoticSSB
 * Contains jump, movement and blink functionality
 * 	As well as collision logic
 */

using System;
using UnityEngine;
using System.Collections;

using KaoticSSB.Weapons;
using KaoticSSB.Globals;

namespace KaoticSSB.Players
{
	public class PlayerBase : MonoBehaviour
	{
		#region EditorVariables
		public float MovementSpeed = 5.0f;
		public float JumpSpeed = 450.0f;
		public int MaxJumps = 2;
		
		public float MinimumBlinkMagnitude = 0.5f;
		public float BlinkDistance = 2.0f;
		public float BlinkCD = 0.0f;
		public float MaxRespawnTimer = 3.0f;

		public float MinJoystickMagnitude = 0.4f;
		
		public float ThrowStrength = 1500.0f;
		public int StartingHealth = 1;
		
		public Transform ParticleBloodEffect;
		public Transform ParticleJumpEffect;
		public Transform ParticleLandEffect;
		#endregion
		
		#region Accessors
		public int PlayerIndex
		{
			get;
			protected set;
		}
		public int Deaths
		{
			get;
			protected set;
		}
		public float RespawnTimer
		{
			get;
			protected set;
		}
		public bool Dead
		{
			get;
			protected set;
		}
		public bool Attacking
		{
			get;
			set;
		}
		#endregion
		
		public PlayerBase ()
		{
		}
		
		public virtual void Init()
		{
			mJumpsRemaining = MaxJumps;
			mHasWeapon = true;
			Dead = false;
			mHealth = StartingHealth;
			Attacking = false;
			mCollider = GetComponent<BoxCollider2D>();

			Animator animator = this.GetComponent<Animator>();
			animator.Play("hero_run");
		}
		
		/**
		 * \brief Called once per frame
		 * Update respawn timer
		 * Check if player has fallen beneath the kill zone
		 * Handle input
		 */
		public virtual void Update()
		{
			mCurrentBlinkCD -= Time.deltaTime;
			if(Dead)
			{
				RespawnTimer -= Time.deltaTime;
			}
			if(gameObject.transform.position.y <= Global.KILL_Y)
			{
				TakeDamage(1);
			}
			//finished attacking
			if(mMeleeWeapon == null)
			{
				Attacking = false;
			}
		}
		
		public virtual void FixedUpdate()
		{
			
		}
		
		/**
		 * \brief Set the index of the player
		 */
		public void SetIndex(int playerIndex)
		{
			PlayerIndex = playerIndex;
			SetController(playerIndex);
		}
		
		/**
		 * \brief Sets up the input strings to the input string with the correct controller ID
		 */
		public void SetController(int controllerID)
		{
			Input_Dodge += controllerID;
			Input_Horizontal += controllerID;
			Input_Jump += controllerID;
			Input_Vertical += controllerID;
		}
		
		#region COLLISION
		void OnCollisionEnter2D(Collision2D collisionObject)
		{
			if(collisionObject != null)
			{
				PerformLand(collisionObject.gameObject);

				if(collisionObject.gameObject.tag == "Throwing")
				{
					ThrowingWeapon weapon = collisionObject.gameObject.GetComponent<ThrowingWeapon>();
					if(weapon != null)
					{
						if(!mHasWeapon && !weapon.Active)
						{
							PickupWeapon();
							mThrowingWeapon = weapon;
							Destroy (collisionObject.gameObject);
						}
						else if(weapon.Active)
						{
							mDirectionOfAttackDegrees = weapon.PreviousAngle;
							mDirectionOfAttack = weapon.PreviousVelocity;
							TakeDamage(1);
						}
					}
				}
			}
		}
		
		void OnCollisionExit2D(Collision2D collisionObject)
		{
			if(collider2D != null)
			{
				if(collider2D.gameObject.tag == "Environment")
				{
					mJumping = true;
				}
			}
		}
		
		/**
		 * \brief Returns the point at which the raycast hit anything tagged "Environment"
		 */
		private Vector2 GetCollisionWithEnvironment(Vector2 raycastStart, Vector2 raycastEnd, float distance)
		{
			RaycastHit2D[] hits = Physics2D.RaycastAll (raycastStart, raycastEnd, distance);
			foreach(RaycastHit2D hit in hits)
			{
				if(hit.collider != null)
				{
					if(hit.collider.tag == "Environment")
					{
						return hit.point;
					}
				}
			}
			return new Vector2(0,0);
		}

		void OnTriggerEnter2D(Collider2D other) 
		{
			if(other.gameObject.tag == "MeleeWeapon")
			{
				MeleeWeapon attackingWeapon = other.gameObject.GetComponent<MeleeWeapon>();
				if(attackingWeapon != null)
				{
					if(mMeleeWeapon == null || mMeleeWeapon != attackingWeapon)
					{
						mDirectionOfAttackDegrees = attackingWeapon.transform.eulerAngles;
						mDirectionOfAttack = attackingWeapon.AttackDirection;
						if(mDirectionOfAttack.x != 0)
						{
							mDirectionOfAttackDegrees = new Vector3(mDirectionOfAttackDegrees.x + 180.0f, mDirectionOfAttackDegrees.y, mDirectionOfAttackDegrees.z);
						}
						TakeDamage(1);
					}
				}
			}
		}
		#endregion
		
		public void PickupWeapon()
		{
			mHasWeapon = true;
		}
		
		/** 
		 * \brief Respawns the player
		 * TODO: Currently uses spawn points ALWAYS, may want to change this so spawn points are mode specific logic
		 */
		public void Respawn()
		{
			Vector3 spawnLocation = new Vector3(0,0,0);
			String spawnName = "SpawnPoint_" + PlayerIndex;
			GameObject spawn = GameObject.Find(spawnName);
			if(spawn == null)
			{
				Debug.Log ("Could not find spawn location for player " + PlayerIndex + "." );
			}
			else
			{
				spawnLocation = spawn.transform.position;
			}
			this.transform.position = spawnLocation;
			
			rigidbody2D.WakeUp();
			Init ();
		}
		
		protected void TakeDamage(int damage)
		{
			if( !Dead )
			{
				mHealth -= damage;
				SpawnBloodEffect();
				
				//check if dead
				if(mHealth <= 0)
				{
					Die ();
				}
			}
		}
		
		/**
		 * \brief Kills the player
		 * Sets up respawning and keeps track of number of deaths
		 */
		protected void Die()
		{
			if(!Dead)
			{
				if(mThrowingWeapon != null)
				{
					Destroy (mThrowingWeapon.gameObject);
				}
				Debug.Log ("Player " + PlayerIndex + " has died");
				Deaths++;
				transform.position = deathLoc;
				Dead = true;
				RespawnTimer = MaxRespawnTimer;
				rigidbody2D.Sleep();
			}
		}

		#region Jump
		/**
		 * \brief adds a y velocity to the character
		 * Does nothing if no jumps are remaining
		 */
		protected void PerformJump()
		{
			if(mJumpsRemaining > 0)
			{
				mJumpsRemaining--;
				rigidbody2D.AddForce (new Vector2(0, JumpSpeed));
				if(mJumpsRemaining == 1)
				{
					SpawnJumpEffect();
				}
			}
		}
		/**
		 * \brief resets jumping
		 * Sets jumping to false and resets jump counter
		 */
		protected void PerformLand(GameObject collided)
		{
			mJumpsRemaining = MaxJumps;
			mJumping = false;
			if(collided.tag =="Environment")
			{
				SpawnLandEffect();
			}
		}
		#endregion Jump
		
		#region Dodge
		/**
		 * \brief Draws the designated blink distance
		 * Green line is the designated blink path
		 * Red line is drawn from the point where the ray passed through Environment
		 */
		protected void Debug_Dodge()
		{
			Vector2 targetPosition = LeftJoystickDirection.normalized * BlinkDistance;
			
			Vector2 hitPoint = GetCollisionWithEnvironment(this.gameObject.transform.position, targetPosition, BlinkDistance);
			Debug.DrawRay (this.gameObject.transform.position, targetPosition, Color.green);
			if(hitPoint.magnitude > 0)
			{
				Debug.DrawLine (this.gameObject.transform.position, hitPoint, Color.red);
			}
		}
		/**
		 * \brief Attempts to perform a blink dodge move
		 * Blink will attempt to blink in the direction of the joystick to the maximum distance specified
		 * If environment is hit, the player will blink as close to the environment as possible
		 * Resets y-velocity to 0 so upward momentum is stopped after the blink
		 */
		protected void PerformDodge()
		{
			if(!Attacking)
			{
				Vector2 targetPosition = LeftJoystickDirection.normalized * BlinkDistance;
				
				// Check to see if the dodge will collide with the environment
				// If so, teleport the player to just above the point at which they were to hit the platform ( hit location + height / 2 )
				Vector2 hit = GetCollisionWithEnvironment(this.gameObject.transform.position, targetPosition, BlinkDistance);
				if(hit.magnitude > 0.0f)
				{
					hit.y += mCollider.size.y;
					Vector2 translation = new Vector2(hit.x - this.gameObject.transform.position.x, hit.y - this.gameObject.transform.position.y);
					targetPosition = translation;
				}
				
				// Only perform the dodge the teleportation amount is greater than or equal to the specified teleportation amount
				// This stops the player from wasting a blink cooldown to teleport to the ground when they are already on the ground
				if(targetPosition.magnitude >= MinimumBlinkMagnitude)
				{
					//figure out where to teleport player based on how the joystick is pressed
					mRightTriggerPressed = true;
					mCurrentBlinkCD = BlinkCD;
					this.gameObject.transform.Translate(targetPosition.x, targetPosition.y, 0);
					
					//reset player y velocity in case they were traveling upwards
					if(rigidbody2D.velocity.y > 0)
					{
						Vector2 currentVelocity = rigidbody2D.velocity;
						currentVelocity.y = 0;
						rigidbody2D.velocity = currentVelocity;
					}
				}
			}
		}
		#endregion
		
		#region Attacks
		/**
		 * \brief Throws the player's weapon in the direction they are holding the left joystick
		 * Uses the same left stick calculation that dodge uses
		 * Also pushes the weapon out of the player's collision box so that they don't colide with their own weapon
		 */
		protected void ThrowWeapon()
		{
			if(mHasWeapon)
			{
				WeaponManager weaponManager = GameObject.FindObjectOfType<WeaponManager>();
				
				//if the left joystick was pressed enough to actually have a viable throw angle
				if(LeftJoystickDirection.magnitude > MinJoystickMagnitude)
				{
					mHasWeapon = false;
					Rigidbody2D weapon = weaponManager.CreateThrowingWeapon(this);
					
					mThrowingWeapon = weapon.GetComponent<ThrowingWeapon>();

					//get the bounds of the player so we can push the weapon outside of the player bounds
					Vector3 bounds = (mCollider.size);
					float boundsDiagonal = (float)Math.Sqrt ((bounds.x * bounds.x) + (bounds.y * bounds.y));
					
					//also get the bounds of the weapon so we push it that much further
					Vector3 weaponBounds = (weapon.renderer.bounds.extents);
					float weaponBoundsDiagonal = (float)Math.Sqrt ((weaponBounds.x * weaponBounds.x) + (weaponBounds.y * weaponBounds.y));
					
					//create weapon outside of player enough so that it doesn't collide with player
					Vector2 newWeaponLocation = (Vector2)weapon.transform.position + (LeftJoystickDirection.normalized * boundsDiagonal) + (LeftJoystickDirection.normalized * weaponBoundsDiagonal);
					weapon.transform.position = newWeaponLocation;
					
					//push the weapon in the way the left joystick was facing
					Vector2 targetPosition = LeftJoystickDirection.normalized * ThrowStrength;
					weapon.AddForce(targetPosition);
				}
			}
		}
		protected void MeleeAttack()
		{
			if(mHasWeapon && !Attacking && LeftJoystickDirection.magnitude > MinJoystickMagnitude)
			{	
				WeaponManager weaponManager = GameObject.FindObjectOfType<WeaponManager>();
				Transform weapon = weaponManager.CreateMeleeWeapon(this);
				mMeleeWeapon = weapon.GetComponent<MeleeWeapon>();

				Quaternion newRotation = weapon.transform.rotation;
				Vector3 offset = new Vector3(0.0f, 0.0f, 0.0f);

				//constrict joystick to 4 dimensions
				//attacking right
				if(LeftJoystickDirection.x >= 0.5f)
				{
					offset.x = weapon.renderer.bounds.extents.x + mCollider.size.x/2;
					newRotation *= Quaternion.Euler(0.0f, 0.0f, 180.0f);
					mMeleeWeapon.AttackDirection = new Vector2(1.0f, 0.0f);
				}
				//attacking left
				else if(LeftJoystickDirection.x <= -0.5f)
				{
					offset.x = -1 * (weapon.renderer.bounds.extents.x + mCollider.size.x/2);
					mMeleeWeapon.AttackDirection = new Vector2(-1.0f, 0.0f);
				}
				//attacking up
				else if(LeftJoystickDirection.y >= 0.5f)
				{
					offset.y = weapon.renderer.bounds.extents.x + mCollider.size.x/2;
					newRotation *= Quaternion.Euler(0.0f, 0.0f, -90.0f);
					mMeleeWeapon.AttackDirection = new Vector2(0.0f, 1.0f);
				}
				//attacking down
				else if(LeftJoystickDirection.y <= -0.5f)
				{
					offset.y = -1 * (weapon.renderer.bounds.extents.x + mCollider.size.x/2); 
					newRotation *= Quaternion.Euler(0.0f, 0.0f, 90.0f);
					mMeleeWeapon.AttackDirection = new Vector2(0.0f, -1.0f);
				}

				Vector3 newPosition = weapon.transform.position + offset;
				weapon.transform.position = newPosition;
				weapon.transform.rotation = newRotation;
				weapon.gameObject.transform.parent = this.transform;
				Attacking = true;
			}
		}
		#endregion
		
		#region Particles
		/**
		 * \brief
		 * Must set mDirectionOfAttack before using this
		 */
		private void SpawnBloodEffect()
		{
			ParticleManager pm = GameObject.FindObjectOfType<ParticleManager>();
			float rotation = mDirectionOfAttackDegrees.z + mDirectionOfAttackDegrees.x;
			rotation += 45.0f + 180.0f;
			rotation = rotation%360.0f;
			Vector3 newPosition = this.gameObject.transform.position;
			newPosition += (mDirectionOfAttack.normalized * 1.5f);
			float scale = 1.0f;
			pm.CreateAnimatedSpriteParticleSystem(ParticleBloodEffect, newPosition, rotation, scale);
		}
		private void SpawnJumpEffect()
		{
			ParticleManager pm = GameObject.FindObjectOfType<ParticleManager>();
			Vector3 newPosition = this.gameObject.transform.position;
			float rotation = -40.0f;
			float scale = 1.0f;
			pm.CreateAnimatedSpriteParticleSystem(ParticleJumpEffect, newPosition, rotation, scale);
		}
		private void SpawnLandEffect()
		{
			ParticleManager pm = GameObject.FindObjectOfType<ParticleManager>();
			Vector3 newPosition = this.gameObject.transform.position;
			float rotation = 0.0f;
			float scale = 1.0f;
			pm.CreateAnimatedSpriteParticleSystem(ParticleLandEffect, newPosition, rotation, scale);
		}
		#endregion
		
		protected ThrowingWeapon mThrowingWeapon;
		protected MeleeWeapon mMeleeWeapon;
		protected Vector2 deathLoc = new Vector2(-9999.9f, -9999.9f);
		
		protected string Input_Jump = "Jump_Player_";
		protected string Input_Dodge = "Dodge_Player_";
		protected string Input_Horizontal = "Horizontal_Player_";
		protected string Input_Vertical = "Vertical_Player_";
		
		protected Vector2 LeftJoystickDirection;
		
		protected int mJumpsRemaining;
		protected bool mJumping;
		protected float mCurrentBlinkCD;
		protected bool mRightTriggerPressed = false;
		protected const bool DEBUG_TEXT_ENABLED = true;
		protected bool mHasWeapon;
		protected int mHealth;
		protected BoxCollider2D mCollider;

		private Vector3 mDirectionOfAttackDegrees;
		private Vector3 mDirectionOfAttack;

	}
}

